package cz.spock.cms.service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.googlecode.objectify.Key;

import cz.spock.cms.entity.CSS;
import cz.spock.cms.entity.Node;
import cz.spock.cms.entity.Page;
import cz.spock.cms.entity.Project;
import cz.spock.cms.entity.content.Field;
import cz.spock.cms.template.entity.TemplateSource;
import cz.spock.core.entity.Account;
import cz.spock.core.service.BaseService;
import cz.spock.exception.InvalidInputException;

/**
 * service class for Node object
 * 
 * provides methods for controllers and also CRUD methods for Node and some for free marker
 * 
 * @author miso
 *
 */
@Service
public class NodeService extends BaseService {
    
    @Autowired
    private FieldService fService;
    @Autowired
    private ProjectService pService;
    
    // NODE DATABASE OPERATIONS
    
    /**
     * If parents list is empty and project is specified, Node is marked as top node/home page
     * 
     * @param childs
     * @param parents
     * @param contentNodes
     * @param contentFields
     * @param url - ONLY PART OF URL - without project prefix...
     * @param label
     * @return created node with id
     */
    public Node createNode(List<Key<Node>> childs, List<Key<Node>> parents, List<Key<Node>> contentNodes, List<Key<Field>> contentFields, String url,
            Key<Project> project, String description) {
        
        Node n = new Node();
        n.setChilds(childs);
        n.setParents(parents);
        n.setContentNodes(contentNodes);
        n.setContentFields(contentFields);
        // TODO set proper url, now setting only url as PROJECT_URL/NODE_URL but should count parents...
        Project p = pService.getProjectById(project.getId());
        if(!url.startsWith("/")) {
            url = p.getUrl() + "/" + url;
        } else {
            url = p.getUrl() + url;
        }
        n.setUrl(url);
        // TODO check label and url unique
        n.setProject(project);
        n.setDescription(description);
        if(project != null && parents == null) {
            //TODO set top node
        }
        n.setId(dao.put(n).getId());
        return n;
    }
    
    public Node createNode(String url, String project, String description) {
        Long l = null;
        try {
            l = Long.parseLong(project);
        } catch(NumberFormatException e) {
            log.fine("project is not number");            
            throw new InvalidInputException("Project must be number.", 411);            
        }
        return this.createNode(null, null, null, null, url, new Key<Project>(Project.class, l), description);
    }
    
    
    @Deprecated // hard to implement, wait if needed
    public List<Node> findNodeByUser(Long id) {
        return null;
    }
    
    @Deprecated // hard to implement, wait if needed
    public List<Node> findNodeByUser(Account acc) {
        return this.findNodeByUser(acc.getId());
    }
    
    /**
     * returns null if node not found or if parameter is null
     * 
     * for parameter "" (empty string) can try to find url
     * @param url
     * @return
     */
    public Node findNodeByUrl(String url) {
        if(url == null) return null;
        return dao.ofy().query(Node.class).filter("url", url).get();
    }
    
    /**
     * @param p - project
     * @return list of nodes for given project id
     */
    public List<Node> findNodeByProject(Project p) {
        if(p == null) return null;
        return this.findNodeByProject(p.getId());
    }
    
    /**
     * @param id - project id
     * @return list of nodes for given project id
     */
    public List<Node> findNodeByProject(Long id) {
        if(id == null) return null;
        Key<Project> key = new Key<Project>(Project.class, id); 
        return dao.ofy().query(Node.class).filter("project", key).list();
    }
    
    /**
     * returns node with given id
     * @param id
     * @return
     */
    public Node getNodeById(Long id) {
        if(id == null) return null;
        return dao.find(Node.class, id);
    }
    
    
    // ------ NODE LOGIC AND UTIL METHODS ------
    
    /**
     * returns true if given node is Page
     */
    public boolean isPage(Node n) {
        if(n == null) return false;
        if(n instanceof Page) {
            return true;
        }
        return false;
    }
    
    /**
     * returns given node as Page objecth
     * @param n
     * @return
     */
    public Page castToPage(Node n) {
        if(n == null) return null;
        if(isPage(n)) {
            return (Page) n;
        }
        return null;
    }
    
    /**
     * returns data for node with given id as tree - result is data model for freemarker
     * 
     * @return
     */
    public Map<String, Object> getDataTree(Node node) {
        if(node == null) return null;
        List<Key<Field>> fields = node.getContentFields();     
        HashMap<String, Object> tree = new HashMap<String, Object>();
        if(fields == null || fields.size() == 0) return tree;
        for(Key<Field> kf:fields) {
            Field f = dao.find(kf);
            if(f != null) {
                tree.put(f.getLabel(), f.getValue());
            }
        }
        return tree;
    }
    
    /**
     * same as getDataTree() but works recursively
     * returns full data tree - all data in current node and all subnodes
     * 
     * @param node
     * @return
     */
    public Map<String, Object> getFullDataTree(Node node) {
        if(node == null) return null;
        List<Key<Field>> fields = node.getContentFields();
        HashMap<String, Object> tree = new HashMap<String, Object>();
        if(fields != null && fields.size() != 0) {
            // non recursive part
            for(Key<Field> kf:fields) {
                Field f = dao.find(kf);
                if(f != null) {
                    tree.put(f.getLabel(), f.getValue());
                }
            }            
        }
        List<Key<Node>> nodes = node.getContentNodes();
        if(nodes != null && nodes.size() != 0) {
            // recursive part
            for(Key<Node> kn:nodes) {
                Node newNode = dao.find(kn);
                if(newNode != null)
                    tree.put(newNode.getLabel(), this.getFullDataTree(newNode));
            }
        }
        return tree;
    }
    
    public String mapToHtml(Map<String, Object> map, int lvl, StringBuilder result) {
        for(Entry e : map.entrySet()) {
            if(e.getValue() instanceof HashMap) {
                result.append(this.addNBSP(lvl-1) + e.getKey() + " { <br/>");
                result.append(this.addNBSP(lvl) + this.mapToHtml((HashMap)e.getValue(), lvl+1, result));
            } else {                
                result.append(this.addNBSP(lvl) + e.getKey() + " : " + e.getValue().toString() + "<br/>");
            }            
            
        }
        return result.toString();
    }

    /**
     * returns string of NBSP - non breaking spaces
     * @param lvl
     * @return
     */
    private String addNBSP(int lvl) {
        StringBuilder r = new StringBuilder(lvl*2*6);
        for(int i = 0; i < lvl*2; i++) {
            r.append("&nbsp;");
        }
        return r.toString();
    }
    
    // TODO move into base service or somewhere...
    /**
     *
     * creates list of strings from string
     * 
     * default delimeters are , and space
     * 
     * @param text
     * @param delimeters - specified delimeters as RE, f.e. "[,; ]" will take , ; and space as possible delimeters
     * @return
     */
    protected List<String> stringToList(String text, String delimeters) {
        if(text == null) return null;
        if(delimeters == null) {
            delimeters = "[, ]";
        }
        List<String> result = null;
        return Arrays.asList(text.split(delimeters));
    }
    
    protected Long stringToLong(String text) {
        if(text == null) return null;
        return Long.valueOf(text);
    }
    
    // TODO implement this one-operation-per-method
    /**
     * creates new node, returns node with assigned id
     */
    public Node createEmptyNode() {
        Node n = new Node();
        n.setId(dao.put(n).getId());
        return n;
    }
    
    public void createParentChildRelation(Long parent, Long child) {
        this.addChild(parent, child);
        this.addParent(child, parent);
    }

    /**
     * adds parent to node
     * 
     * @param nodeId
     * @param parentId
     * @return changed node, or null if change was not successful or if parameters were null
     */
    public Node addParent(Long nodeId, Long parentId) {
        return this.universalNodeAdd(nodeId, parentId, NodeListType.PARENT);
    }
    
    /**
     * adds parent to node
     * 
     * @param nodeId
     * @param parentId
     * @return changed node
     */
    public Node addParent(Node node, Node parent) {
        return this.universalNodeAdd(node, parent, NodeListType.PARENT);
    }
    
    /**
     * removes parent node from node if present
     * returns null if params invalid, otherwise returns node without parent 
     * 
     * @param nodeId
     * @param parentId
     * @return node without parent node or null
     */
    public Node removeParent(Long nodeId, Long parentId) {
        return this.universalNodeRemove(nodeId, parentId, NodeListType.PARENT);
    }
    
    /**
     * removes parent node from node if present
     * 
     * must be called only with valid parameters - use removeParent(Long id, Long id) method
     * 
     * @param nodeId
     * @param parentId
     * @return node without parent node
     */
    public Node removeParent(Node node, Node parent) {
        return this.universalNodeRemove(node, parent, NodeListType.PARENT);
    }
    
    /**
     * adds node as child to given node
     * 
     * @param nodeId
     * @param childId
     * @return modified node or null
     */
    public Node addChild(Long nodeId, Long childId) {
        return this.universalNodeAdd(nodeId, childId, NodeListType.CHILD);
    }
    
    /**
     * adds node as child to given node
     * 
     * @param nodeId
     * @param childId
     * @return modified node
     */
    public Node addChild(Node node, Node child) {
        return this.universalNodeAdd(node, child, NodeListType.CHILD);
    }
    
    
    
    public Node removeChild(Long nodeId, Long childId) {
        return this.universalNodeRemove(nodeId, childId, NodeListType.CHILD);
    }
    
    /**
     * adds node as child to given node
     * 
     * @param nodeId
     * @param childId
     * @return modified node
     */
    public Node removeChild(Node node, Node child) {
        return this.universalNodeRemove(node, child, NodeListType.CHILD);
    }
    
    
    
    public Node addContentNode(Long nodeId, Long contId) {
        return this.universalNodeAdd(nodeId, contId, NodeListType.NODE);
    }
    
    public Node addContentNode(Node node, Node cont) {
        return this.universalNodeAdd(node, cont, NodeListType.NODE);
    }
    
    public Node removeContentNode(Long nodeId, Long contId) {
        return this.universalNodeAdd(nodeId, contId, NodeListType.NODE);
    }
    
    
    public Node addContentField(Long nodeId, Long fieldId) {
        if(nodeId == null || fieldId == null) {
            // throw exception - that means proper exception with error message and number
        }
        Node n = this.getNodeById(nodeId);
        if(n == null) {
            
            return null;
        }
        Field f = fService.getFieldById(fieldId);
        if(f == null) {
            
            return null;
        }
        return this.addContentField(n, f);
        
    }
    
    public Node addContentField(Node node, Field field) {
        List<Key<Field>> list = node.getContentFields();
        if(list == null) {
            list = new ArrayList<Key<Field>>();
            list.add(new Key<Field>(Field.class, field.getId()));
            node.setContentFields(list);
        } else {
            list.add(new Key<Field>(Field.class, field.getId()));
        }
        dao.ofy().put(node);
        return node;
    }
    
    public Node removeContentField(Long nodeId, Long fieldId) {
        if(nodeId == null || fieldId == null) {
            // throw exception - that means proper exception with error message and number
        }
        Node n = this.getNodeById(nodeId);
        if(n == null) {
            
            return null;
        }
        Field f = fService.getFieldById(fieldId);
        if(f == null) {
            
            return null;
        }
        return this.removeContentField(n, f);
    }
    
    public Node removeContentField(Node node, Field field) {
        List<Key<Field>> list = node.getContentFields();
        if(list == null) {
            return node;
        } else {
            list.remove(new Key<Field>(Field.class, field.getId()));
        }
        dao.ofy().put(node);
        return node;
    }
    
    public Node setTemplate(Long tempId, Long nodeId) {
        Node n = null;
        n = dao.get(Node.class, nodeId);
        if(n == null) {
            // TODO handle node not found
            return null;
        }
        n.setTemplate(new Key<TemplateSource>(TemplateSource.class, tempId));
        dao.put(n);
        return n;
    }
    
    
    public Node setStyle(Long styleId, Long nodeId) {
        Node n = null;
        n = dao.get(Node.class, nodeId);
        if(n == null) {
            // TODO handle node not found
            return null;
        }
        return this.setStyle(styleId, n);
    }
    
    public Node setStyle(Long styleId, Node node) {
        node.setCss(new Key<CSS>(CSS.class, styleId));
        dao.put(node);
        return node;
    }
    
    public Node setStyle(CSS style, Node node) {
        return this.setStyle(style.getId(), node);
    }
    

    // PRIVATE METHODS
    
    // TODO exceptions
    private Node universalNodeAdd(Long nodeId, Long objId, NodeListType mode) {
        if(nodeId == null || objId == null) {
            // throw exception - that means proper exception with error message and number
        }
        Node n = this.getNodeById(nodeId);
        if(n == null) {
            
            return null;
        }
        Node o = this.getNodeById(objId);
        if(o == null) {
            
            return null;
        }
        return this.universalNodeAdd(n, o, mode);
    }
    
    private Node universalNodeAdd(Node node, Node object, NodeListType mode) {
        List<Key<Node>> list = null;
        switch(mode) {
            case PARENT:{
                list = node.getParents(); 
                break;
            }
            case CHILD: {
                list = node.getChilds();
                break;
            }
            case NODE: {
                list = node.getContentNodes();
                break;
            }
        }
        if(list == null) {
            list = new ArrayList<Key<Node>>();
            list.add(new Key<Node>(Node.class, object.getId()));
            node.setChilds(list);
        } else {
            list.add(new Key<Node>(Node.class, object.getId()));
        }
        dao.ofy().put(node);
        return node;
    }

    // TODO exceptions
    private Node universalNodeRemove(Long nodeId, Long objId, NodeListType mode) {
        if(nodeId == null || objId == null) {
            // throw exception - that means proper exception with error message and number
        }
        Node n = this.getNodeById(nodeId);
        if(n == null) {
            
            return null;
        }
        Node o = this.getNodeById(objId);
        if(o == null) {
            
            return null;
        }
        return this.universalNodeAdd(n, o, mode);
    }
    
    private Node universalNodeRemove(Node node, Node object, NodeListType mode) {
        List<Key<Node>> list = null;
        switch(mode) {
            case PARENT:{
                list = node.getParents();
                break;
            }
            case CHILD: {
                list = node.getChilds();
                break;
            }
            case NODE: {
                list = node.getContentNodes();
                break;
            }
        }
        if(list == null) {
            return node;
        } else {
            list.remove(new Key<Node>(Node.class, object.getId()));
        }
        dao.ofy().put(node);
        return node;
    }
    
    /**
     * enumeration used in helper methods
     * 
     * @author miso
     */
    public enum NodeListType {
        PARENT, CHILD, NODE, FIELD
    }


}
